/**
 * Copyright (c) 2007, Blue Hole Software. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */
package org.bhf.security.common;

import org.bhf.facilities.ProjectInjectors;
import org.bhf.security.authorization.RoleBasedSecurityManager;
import org.bhf.security.authorization.HierarchicalPermisionDelegate;
import org.bhf.security.authorization.ParameterizablePermission;
import org.bhf.security.util.Templates;

import java.io.Serializable;
import java.security.Permission;
import java.security.PermissionCollection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;

/**
 * This <code>Permission</code> protected modification of a security policy. The permission
 * path (name) is the class name of the permission to be added to or removed from a project
 * policy. The permission has no associated asctions.
 */
public final class PolicyPermission extends Permission implements ParameterizablePermission, Serializable
{
    private static final long serialVersionUID = 7930732926638008763L;

    final String                        originalActions;
    final HierarchicalPermisionDelegate delegate;

    /**
     * Creates a new PolicyPermission for the given path. The actions paramter
     * is ignored.
     *
     * @param path the path of the file/directory.
     * @param actions the action string.
     */
    public PolicyPermission( final String path, final String actions )
    {
        super( path );
        this.originalActions    = actions;
        delegate                = new HierarchicalPermisionDelegate( path, actions );
    }

    /**
     * Creates a new PolicyPermission for the <code>Permission</code> as
     * a convenience for application code performing check permissions
     * prior to modifying a policy.
     *
     * @param permission <code>Permission</code> being consider for addition to or removal
     *  from a policy.
     */
    public PolicyPermission( final Permission permission )
    {
        this( permission.getClass().getName(), null );
    }

    /**
     * Implements the guard interface for a permission. The <code>RoleBasedSecurityManager.check</code>
     * method is called, passing this permission object as the permission to check. Returns silently if access
     * is granted. Otherwise, throws a <code>SecurityException</code>.
     * @param object the object being guarded (currently ignored).
     * @throws java.lang.SecurityException Access denied.
     */
    public void     checkGuard( final Object object )
        throws SecurityException
    {
        ProjectInjectors.getProjectInjector().getInstance( RoleBasedSecurityManager.class ).check( this );
    }

    /**
     * Apply parameters to the permission.
     * @param parameters The substitution Strings - key (String) to value (String).
     * @return A clone of the original <code>Permission</code> with the paramters applied to the path and actions
     *      expressions.
     */
    public Permission[] parameterize( final Map<String,String[]> parameters )
    {
        final int   l = Templates.lengthOfPluralReference( getName() + getActions(), parameters );

        // None
        if( l == 0 )
        {
            return new Permission[] { this };
        }
        else
        {
            final Permission[] permissions = new Permission[ l ];

            for( int i = 0; i < permissions.length; i++ )
                permissions[i] = new PolicyPermission(
                    Templates.process( getName(), parameters, i ),
                    null
                );

            return permissions;
        }
    }

    /**
     * Checks if this PolicyPermission object "implies" the specified permission.
     * <P>
     * More specifically, this method returns true if:<p>
     * <ul>
     * <li> <i>p</i> is an instanceof PolicyPermission,</li>
     * <li> <i>p</i>'s path is implied by this object's
     *      path. For example, "/tmp/*" implies "/tmp/foo", since
     *      "/tmp/*" encompasses the "/tmp" directory and all files in that
     *      directory, including the one named "foo".</li>
     * <li> this object is a grant type permission </li>
     * </ul>
     * @param p the permission to check against.
     *
     * @return true if the specified permission is implied by this object,
     * false if not.
     */
    public boolean implies( final Permission p )
    {
        return !(!(p instanceof PolicyPermission) || this.delegate.isDeny()) && this.delegate.implies( ((PolicyPermission) p).delegate );
    }

    /**
     * Checks if this PolicyPermission object "denies" the specified permission.
     * <P>
     * More specifically, this method returns true if:<p>
     * <ul>
     * <li> <i>p</i> is an instanceof PolicyPermission,</li>
     * <li> <i>p</i>'s path is implied by this object's
     *      path. For example, "/tmp/*" implies "/tmp/foo", since
     *      "/tmp/*" encompasses the "/tmp" directory and all files in that
     *      directory, including the one named "foo".</li>
     * <li> this object is a deny type permission </li>
     * </ul>
     * @param p the permission to check against.
     *
     * @return true if the specified permission is implied by this object,
     * false if not.
     */
    public boolean denies( final Permission p )
    {
        return !(!(p instanceof PolicyPermission) || !this.delegate.isDeny()) && this.delegate.implies( ((PolicyPermission) p).delegate );

    }

    /**
     * Checks two PolicyPermission objects for equality. Checks that <i>obj</i> is
     * a PolicyPermission, and has the same path and actions as this object.
     * <P>
     * @param obj the object we are testing for equality with this object.
     * @return true if obj is a PolicyPermission, and has the same path and
     * actions as this PolicyPermission object.
     */
    public boolean equals( Object obj )
    {
        if( obj == this )
            return true;

        if( !(obj instanceof PolicyPermission) )
            return false;

        PolicyPermission that = (PolicyPermission)obj;
        return this.delegate.equals( that.delegate );
    }

    /**
     * Returns the hash code value for this object.
     *
     * @return a hash code value for this object.
     */
    public int hashCode()
    {
        return delegate.hashCode();
    }

    /**
     * Returns the "canonical string representation" of the actions.
     * That is, this method always returns present actions in the following order:
     * read, write, execute, delete. For example, if this PolicyPermission object
     * allows both write and read actions, a call to <code>getActions</code>
     * will return the string "read,write".
     *
     * @return the canonical string representation of the actions.
     */
    public String getActions()
    {
        return delegate.getActions();
    }

    /**
     * Returns a new PermissionCollection object for storing PolicyPermission
     * objects.
     * <p>
     * PolicyPermission objects must be stored in a manner that allows them
     * to be inserted into the collection in any order, but that also enables the
     * PermissionCollection <code>implies</code>
     * method to be implemented in an efficient (and consistent) manner.
     *
     * <p>For example, if you have two FilePermissions:
     * <OL>
     * <LI>  <code>"/tmp/-", "read"</code>
     * <LI>  <code>"/tmp/scratch/foo", "write"</code>
     * </OL>
     *
     * <p>and you are calling the <code>implies</code> method with the PolicyPermission:
     *
     * <pre>
     *   "/tmp/scratch/foo", "read,write",
     * </pre>
     *
     * then the <code>implies</code> function must
     * take into account both the "/tmp/-" and "/tmp/scratch/foo"
     * permissions, so the effective permission is "read,write",
     * and <code>implies</code> returns true. The "implies" semantics for
     * FilePermissions are handled properly by the PermissionCollection object
     * returned by this <code>newPermissionCollection</code> method.
     *
     * @return a new PermissionCollection object suitable for storing
     * FilePermissions.
     */

    public PermissionCollection newPermissionCollection()
    {
        return new PolicyPermissionCollection();
    }
}

/**
 * A PolicyPermissionCollection stores a set of PolicyPermission permissions.
 * PolicyPermission objects
 * must be stored in a manner that allows them to be inserted in any
 * order, but enable the implies function to evaluate the implies
 * method.
 * For example, if you have two FilePermissions:
 * <OL>
 * <LI> "/tmp/-", "read"
 * <LI> "/tmp/scratch/foo", "write"
 * </OL>
 * And you are calling the implies function with the PolicyPermission:
 * "/tmp/scratch/foo", "read,write", then the implies function must
 * take into account both the /tmp/- and /tmp/scratch/foo
 * permissions, so the effective permission is "read,write".
 *
 * @see Permission
 * @see PermissionCollection
 */
final class PolicyPermissionCollection extends PermissionCollection implements Serializable
{
    private static final long serialVersionUID = 7930732926638008763L;

    private final ArrayList<PolicyPermission> permissions = new ArrayList<PolicyPermission>();

    /**
     * Create an empty FilePermissions object.
     */
    public PolicyPermissionCollection()
    {
    }

    /**
     * Adds a permission to the PolicyPermissions. The key for the hash is
     * permission.path.
     *
     * @param permission the Permission object to add.
     *
     * @exception IllegalArgumentException - if the permission is not a
     *                                       PolicyPermission
     *
     * @exception SecurityException - if this PolicyPermissionCollection object
     *                                has been marked readonly
     */

    public void     add( final Permission permission )
    {
        if( !(permission instanceof PolicyPermission) )
            throw new IllegalArgumentException( "invalid permission: " + permission );
        if( isReadOnly() )
            throw new SecurityException( "attempt to add a Permission to a readonly PermissionCollection" );

        final PolicyPermission policyPermission = (PolicyPermission)permission;

        synchronized( permissions )
        {
            final HierarchicalPermisionDelegate y = policyPermission.delegate;

            for( final Iterator<PolicyPermission> i = permissions.iterator(); i.hasNext(); )
            {
                final HierarchicalPermisionDelegate x = i.next().delegate;

                // Hit an existing permission that is identical expect possible for the deny flag
                // In either case, deny or grant, the result will be a single permission. If the
                // permissions are the same, this operation is a noop. If they differ, the one
                // being added cancels the existing permission. In either case, we're done with
                // the loop. TO make the logic clean, we simply remove th old permission. If the permissions
                // are the same, this is not really needed since the permission are equal, but we avoid a branch
                if(
                    x.getCanonicalPath().equals( y.getCanonicalPath() ) &&
                    //x.getActions().equals( y.getActions() ) &&
                    x.isDirectory() == y.isDirectory() &&
                    x.isRecursive() == y.isRecursive()
                )
                {
                    i.remove();
                    break;
                }
            }

            permissions.add( policyPermission );
        }
    }

    /**
     * Check and see if this set of permissions implies the permissions
     * expressed in "permission".
     *
     * @param permission the Permission object to compare
     *
     * @return true if "permission" is a proper subset of a permission in
     * the set, false if not.
     */

    public boolean  implies( final Permission permission )
    {
        if( !(permission instanceof PolicyPermission) )
            return false;

        final HierarchicalPermisionDelegate y           = ((PolicyPermission)permission).delegate;
              int                           specificity = 0;
              boolean                       implied     = false;

        synchronized( permissions )
        {
            for( final PolicyPermission permission1 : permissions )
            {

                // It must be more specific than the last match to be considered
                if( permission1.getName().length() > specificity && permission1.delegate.implies( y ) )
                {
                    implied = !permission1.delegate.isDeny();
                    specificity = permission1.getName().length();
                }
            }
        }

        return implied;
    }

    /**
     * Returns an enumeration of all the PolicyPermission objects in the
     * container.
     *
     * @return an enumeration of all the PolicyPermission objects.
     */
    @Override
    public Enumeration<Permission>  elements()
    {
        final Iterator<PolicyPermission> i = permissions.iterator();
        return new Enumeration<Permission>()
        {
            /**
             * Tests if this enumeration contains more elements.
             *
             * @return  <code>true</code> if and only if this enumeration object
             *           contains at least one more element to provide;
             *          <code>false</code> otherwise.
             */
            public boolean hasMoreElements()
            {
                return i.hasNext();
            }

            /**
             * Returns the next element of this enumeration if this enumeration
             * object has at least one more element to provide.
             *
             * @return     the next element of this enumeration.
             * @exception  NoSuchElementException  if no more elements exist.
             */
            public PolicyPermission nextElement()
            {
                return i.next();
            }
        };
    }
}



